#include "xxx_info.h"
#include <libmount/libmount.h>
#include <unistd.h>
#include <libgen.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>

extern bool check_xxx_info_file_inode(ext2_filsys ext2_fs);

/*
 * Use a super_area struct to initialize a empty xxx_info struct.
 *  @info: pointer to empty xxx_info struct
 *  @sa:   pointer to xxx_super_area, usually referred to default_xxx_super_area
 *  return:
 *      On success, return zero.
 */
int init_empty_xxx_info(struct xxx_info *info, const struct xxx_super_area *sa)
{
    if (info == NULL || sa == NULL)
        return 1;
    if (sa->xxx_info_length > 1024 * 1024 || sa->journal_item_count < 10 || sa->mapping_item_count < 10)
        return 2;
    void *buff = malloc(sa->xxx_info_length);
    if (buff == NULL)
        return 3;
    else
    {
        memset(buff, 0, sa->xxx_info_length);
        memcpy(buff, sa, sizeof(*sa));
    }

    info->buff = buff;
    info->buff_length = sa->xxx_info_length;
    info->super_area = (struct xxx_super_area *)buff;
    info->journal_items = (struct xxx_journal_item *)((char *)buff + sa->journal_area_offset);
    info->mapping_items = (struct xxx_mapping_item *)((char *)buff + sa->mapping_area_offset);
    info->journal_item_count = sa->journal_item_count;
    info->mapping_item_count = sa->mapping_item_count;

    return 0;
}


void free_xxx_info_buff(struct xxx_info *info)
{
    if (info != NULL && info->buff != NULL)
    {
        free(info->buff);
        info->buff = NULL;
    }
}

/*
 * Read xxx_info_file content to xxx_info struct.
 *  @info: pointer to a loaded xxx_info struct
 *  @opened_file: pointer to xxx_info_file
 *  return:
 *      On success, return zero.
 */
int read_xxx_info2(struct xxx_info *info, ext2_file_t opened_file)
{
    if (info == NULL || opened_file == NULL)
        return 1;

    unsigned int ret = 0;
    struct xxx_super_area sa;
    ext2fs_file_lseek(opened_file, 0, EXT2_SEEK_SET, &ret); // rewind to start
    ext2fs_file_read(opened_file, &sa, sizeof(struct xxx_super_area), &ret);

    if (ret != sizeof(struct xxx_super_area) || sa.magic_number != XXX_MAGIC_NUM)
        return 2;

    void *buff = malloc(sa.xxx_info_length);
    if (buff == NULL)
        return 3;

    ext2fs_file_lseek(opened_file, 0, EXT2_SEEK_SET, &ret); // rewind to start
    ext2fs_file_read(opened_file, buff, sa.xxx_info_length, &ret);
    if (ret != sa.xxx_info_length)
        return 4;

    info->buff = buff;
    info->buff_length = sa.xxx_info_length;
    info->super_area = (struct xxx_super_area *)buff;
    info->journal_items = (struct xxx_journal_item *)((char *)buff + sa.journal_area_offset);
    info->mapping_items = (struct xxx_mapping_item *)((char *)buff + sa.mapping_area_offset);
    info->journal_item_count = sa.journal_item_count;
    info->mapping_item_count = sa.mapping_item_count;

    if (info->super_area->magic_number != XXX_MAGIC_NUM)
        return 5;

    return 0;
}

/*
 * Write the buffer of xxx_info to xxx_info_file.
 *  @info: pointer to a loaded xxx_info struct
 *  @opened_file: pointer to xxx_info_file
 *  return:
 *      On success, return zero.
 */
int write_xxx_info2(const struct xxx_info *info, ext2_file_t opened_file)
{
    if (info == NULL || info->buff == NULL || opened_file == NULL)
        return 1;

    unsigned int written = 0;
    ext2fs_file_write(opened_file, info->buff, info->buff_length, &written);

    if (written == info->buff_length)
        return 0;
    else
        return 2;
}

/*
 *  Load xxx_info from the path, which is the first step to use xxx lib.
 *  It will do these things:
 *      1. init mount table using libmount.so
 *      2. find the mount point and device path of given path
 *      3. pass the mount point and device path to struct xxx_info
 *      4. try to open fs and read xxx_info if it exists
 *
 *  @info: pointer to a empty xxx_info
 *  @path: path that belong to a ext4 filesystem
 *  return:
 *      On success, return zero.
 *      If xxx_info is not exists on target device, return -1.
*/
int load_xxx_info(struct xxx_info *info, const char *path)
{
    // check path exist
    if (access(path, F_OK ) == -1)
    {
        // file doesn't exist
        return 1;
    }

    // init mount table, set info->table
    info->table = mnt_new_table();
    mnt_table_parse_mtab(info->table, NULL);

    struct libmnt_fs *mnt_fs = mnt_table_find_mountpoint(info->table, path, MNT_ITER_FORWARD);
    if (strcmp("ext3", mnt_fs_get_fstype(mnt_fs)) != 0 && strcmp("ext4", mnt_fs_get_fstype(mnt_fs)) != 0)
    {
        fprintf(stderr, "%s\n  target fs: %s\n",
                "Cannot create xxx_info_file on filesystem other than ext4 or ext3, ",
                mnt_fs_get_fstype(mnt_fs));
        return 1;
    }

    // open ext4 fs, set info->fs info->devpath
    const char *devpath = mnt_fs_get_srcpath(mnt_fs);
    if (devpath == NULL)
        return 2;
    info->devpath = devpath;
    info->mountpoint = mnt_fs_get_target(mnt_fs);
    if (info->mountpoint == NULL)
        return 2;

    ext2_filsys ext2_fs = NULL;
    errcode_t err_code = 0;
    err_code = ext2fs_open(devpath, EXT2_FLAG_RW, 0, 0, unix_io_manager, &ext2_fs);
    if (err_code != 0 || ext2_fs == NULL)
        return 3;

    info->fs = ext2_fs;

    if (check_xxx_info_file_inode(ext2_fs) == false)
        return -1; // xxx_info_file not exist


    // open file for reading
    ext2_file_t file = NULL;
    err_code = ext2fs_file_open(ext2_fs, EXT2_UNDEL_DIR_INO, EXT2_FILE_WRITE , &file);
    if (err_code != 0 || file == NULL)
        return 4;

    int ret = read_xxx_info2(info, file);
    printf(" read %d\n", ret);
    if (ret != 0)
        return 5;

    ext2fs_file_close(file);

    return 0;
}

/*
 * Wrapper of write_xxx_info
 */
inline void flush_xxx_info(const struct xxx_info * const info)
{
    if (info != NULL && info->fs != NULL)
        write_xxx_info(info);
}

void free_xxx_info(struct xxx_info *info)
{
    if (info == NULL)
        return;
    if (info->fs != NULL)
    {
        write_xxx_info(info);
        ext2fs_close(info->fs);
        info->fs = NULL;
    }
    if (info->table != NULL)
    {
        mnt_unref_table(info->table);
        info->fs = NULL;
    }
    free_xxx_info_buff(info);
}

/*
 * Wrapper of read_xxx_info2
 */
int read_xxx_info(struct xxx_info *info)
{
    if (info == NULL || info->fs == NULL)
        return 1;
    // open file for reading
    ext2_file_t file = NULL;
    errcode_t err_code = ext2fs_file_open(info->fs, EXT2_UNDEL_DIR_INO, EXT2_FILE_WRITE , &file);
    if (err_code != 0 || file == NULL)
        return 4;

    int ret = read_xxx_info2(info, file);
    if (ret != 0)
        return 5;

    ext2fs_file_close(file);
    return 0;
}

/*
 * Wrapper of write_xxx_info2
 */
int write_xxx_info(const struct xxx_info *info)
{
    if (info == NULL || info->buff == NULL || info->fs == NULL)
        return 1;

    // open file for reading
    ext2_file_t file = NULL;
    errcode_t err_code = ext2fs_file_open(info->fs, EXT2_UNDEL_DIR_INO, EXT2_FILE_WRITE , &file);

    if (err_code != 0 || file == NULL)
        return 2;

    unsigned int written = 0;
    ext2fs_file_write(file, info->buff, info->buff_length, &written);
    ext2fs_file_flush(file);

    if (written == info->buff_length)
        return 0;
    else
        return 3;
}

/*
 * Resolve realpath of filepath and dirpath then check whether file and dir are on info->fs
 */
int three_in_one(const struct xxx_info *info, const char *filepath,  char *filename, char *dirpath)
{
    // get full path, dir path, filename
    char rp[PATH_MAX];
    char rp1[PATH_MAX];
    char rp2[PATH_MAX];

    if (realpath(filepath, rp) == NULL)
        return 1;

    struct stat file_st = {};
    if (stat(rp, &file_st) != 0)
        return 2;

    strncpy(rp1, rp, PATH_MAX);
    strncpy(rp2, rp, PATH_MAX);

    char *_dirpath = dirname(rp1);
    strncpy(dirpath, _dirpath, PATH_MAX);

    char *_filename = basename(rp2);
    strncpy(filename, _filename, PATH_MAX);


    // check whether info, file, dir are on the same fs
    struct libmnt_fs *file_mnt_fs = mnt_table_find_mountpoint(info->table, rp, MNT_ITER_FORWARD);
    struct libmnt_fs *dir_mnt_fs = mnt_table_find_mountpoint(info->table, dirpath, MNT_ITER_FORWARD);
    if (file_mnt_fs != dir_mnt_fs || strcmp(mnt_fs_get_srcpath(file_mnt_fs), info->devpath) != 0)
    {
        fprintf(stderr, "hide_file(show_file) failed, not on the same file system\n"
                        " file: %s\n dir: %s\n info: %s\n",
                mnt_fs_get_srcpath(file_mnt_fs),
                mnt_fs_get_srcpath(dir_mnt_fs),
                info->devpath);
        return 3;
    }

    return 0;
}
